/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Windows.Forms;
using System.Threading;
using DaveSexton.DocProject.Resources;

namespace DaveSexton.DocProject
{
	/// <summary>
	/// Proxy for <see cref="BuildProcessComponent" />s that are loaded into another <see cref="AppDomain" />.
	/// </summary>
	/// <remarks>
	/// This class is required so that the <see cref="AppDomain" /> may be unloaded and the <see cref="BuildProcessComponent" /> 
	/// released after each build has finished; otherwise, the DocProject will not be able to build again since the target assembly 
	/// would remain in use, even after the assembly reference and all of its objects have gone out-of-scope.
	/// <para>
	/// The proxy is used as a two-way interface for marshaling calls into and out of the new <see cref="AppDomain" />.
	/// </para>
	/// </remarks>
	internal sealed class BuildProcessComponentProxy : BuildProcessComponent
	{
		#region Private / Protected
		internal BuildContext Context
		{
			get
			{
				return buildContext;
			}
		}

		/// <summary>
		/// Marshals calls from the new <see cref="AppDomain" /> back to the main program for VS automation.
		/// This instance is the remote proxy for the local domain.  It's containng proxy is the local proxy for the remote domain.
		/// ("Local" meaning the domain running the UI thread).
		/// </summary>
		public BuildProcessComponentProxy Marshaler
		{
			get
			{
				return marshaler;
			}
			set
			{
				marshaler = value;
			}
		}

		private delegate DialogResult ShowDialogAsync();

		private BuildProcessComponent component;
		private BuildProcessComponentProxy marshaler;
		private readonly BuildContext buildContext;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="BuildProcessComponentProxy" /> class for internal use only.
		/// </summary>
		/// <remarks>This constructor creates an instance of the class to be used as a marshaler for invoking calls from the secondary 
		/// <see cref="AppDomain" /> in which the <see cref="BuildProcessComponent" /> has been loaded to the 
		/// <see cref="AppDomain" /> in which the instance of this class is constructed.  The specified <paramref name="context" /> 
		/// is used to marshal calls to the UI thread if the current <see cref="BuildStep" /> is executing on a background thread.
		/// </remarks>
		private BuildProcessComponentProxy(BuildContext buildContext)
		{
			if (buildContext == null)
				throw new ArgumentNullException("buildContext");

			this.buildContext = buildContext;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="BuildProcessComponentProxy" /> class for internal use only.
		/// </summary>
		/// <remarks>This constructor creates an instance of the class to be used as a proxy for the <see cref="BuildController" /> to 
		/// invoke methods on the <see cref="BuildProcessComponent" /> that has been loaded into another <see cref="AppDomain" />.
		/// </remarks>
		private BuildProcessComponentProxy()
		{
		}
		#endregion

		#region Methods
		public static BuildProcessComponentProxy CreateInstance(string assemblyFile, string typeName, string basePath, 
			BuildContext context, out AppDomain domain)
		{
			AppDomainSetup setup = new AppDomainSetup();
			setup.ApplicationBase = basePath;
			setup.ApplicationName = "DocProject BuildProcessComponent";

			domain = AppDomain.CreateDomain("BuildProcessComponent Domain", null, setup);
			domain.UnhandledException += domain_UnhandledException;

			try
			{
				BuildProcessComponentProxy proxy = (BuildProcessComponentProxy) domain.CreateInstanceAndUnwrap(
					Assembly.GetExecutingAssembly().FullName, typeof(BuildProcessComponentProxy).FullName, false,
					BindingFlags.NonPublic | BindingFlags.Instance, null, null, null, null, null);

				// create remote proxy to the local domain
				proxy.Marshaler = new BuildProcessComponentProxy(context);

				// initialize in new AppDomain
				proxy.Initialize(assemblyFile, typeName);

				return proxy;
			}
			catch
			{
				if (domain != null)
					AppDomain.Unload(domain);

				throw;
			}
		}

		[System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.LinkDemand,
			Flags = System.Security.Permissions.SecurityPermissionFlag.Infrastructure)]
		public override object InitializeLifetimeService()
		{
			return null;
		}

		/// <summary>
		/// Executes in the new AppDomain.
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFile")]
		private void Initialize(string assemblyFile, string typeName)
		{
			System.Reflection.Assembly assembly = (assemblyFile != null) ? System.Reflection.Assembly.LoadFile(assemblyFile) : null;

			Type type = (assembly != null) ? assembly.GetType(typeName, false, false) : Type.GetType(typeName, false, false);

			if (type == null)
				throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
					Errors.BPCTypeNotFound, Environment.NewLine, typeName, assemblyFile));

			Type typeBPC = typeof(BuildProcessComponent);

			if (type != null && typeBPC.IsAssignableFrom(type))
				component = (BuildProcessComponent) Activator.CreateInstance(type);
			else
				// The likeliest cause of error is multiple versions of the DaveSexton.DocProject assembly installed in the GAC or 
				// found in a probe path.  The error message contains useful diagnosticts information (be sure to pay attention to the public key too): 
				throw new ArgumentException(string.Format(System.Globalization.CultureInfo.CurrentCulture, 
					"{1}{0}Specified Base Type: {2};{0}  {3}{0}Expected Type: {4}{0}  {5}", Environment.NewLine,
					Errors.BPCInvalidType, type.BaseType.AssemblyQualifiedName, type.BaseType.Assembly.CodeBase,
					typeBPC.AssemblyQualifiedName, typeBPC.Assembly.CodeBase), "typeName");

			component.Initialize(marshaler);
		}

		public override void BuildStarting(BuildContext context)
		{
			component.BuildStarting(context);
		}

		public override bool BeforeExecuteStep(DaveSexton.DocProject.Engine.IBuildStep step, BuildContext context)
		{
			return component.BeforeExecuteStep(step, context);
		}

		public override void AfterExecuteStep(DaveSexton.DocProject.Engine.IBuildStep step, BuildContext context)
		{
			component.AfterExecuteStep(step, context);
		}

		public override void BuildCompleted(BuildContext context)
		{
			component.BuildCompleted(context);
		}

		public DialogResult ShowDialogInternal(Form form)
		{
			return (DialogResult) buildContext.Invoke((ShowDialogAsync) delegate()
			{
				return form.ShowDialog(DocProjectEnvironment.MainWindow);
			});
		}

		public void ShowChildInternal(Form form)
		{
			buildContext.Invoke(delegate()
			{
				form.Show(DocProjectEnvironment.MainWindow);
			});
		}

		public override string ToString()
		{
			if (component != null)
				return component.ToString();
			else
				return base.ToString();
		}
		#endregion

		#region Event Handlers
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes")]
		private static void domain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
		{
			Exception ex = e.ExceptionObject as Exception;

			Log.Exception(ex ?? new Exception((e.ExceptionObject == null) ? Errors.UnknownError : e.ExceptionObject.ToString()),
				Errors.BPCProxyStepAppDomainUnhandledException, e.IsTerminating);
		}
		#endregion
	}
}
